/*
 *  Copyright 2010 Wesley Gooch, Ryan Nix, Cody Wenzel.
 *  Montana State University - Spring Semester 2010
 *  Computer Science 450 - Compilers
 *  Project: Micro-Pascal Compiler
 */

package msu.compiler.codegenerators;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Stack;
import msu.compiler.errorhandling.ErrorHandler;
import msu.compiler.scanner.Token;
import msu.compiler.semantics.AbstractMethodSymbol;
import msu.compiler.semantics.ParameterSymbol;
import msu.compiler.semantics.SemanticAnalyzer;
import msu.compiler.semantics.SemanticRecord;
import msu.compiler.semantics.Symbol;
import msu.compiler.semantics.VariableSymbol;

/**
 *
 * @author ryan.nix
 */

public class CodeGen {
    private String fileName = "outputFile.txt";
    private SemanticAnalyzer analyzer;
    private BufferedWriter out;
    private Stack<Byte> typeStack;
    private Stack<String> falseLabels;
    private Stack<String> endLabels;
    private Stack<String> labelStack;
    private Stack<String> loopLabels;
    //private Symbol
    private int register;
    boolean screenlog = false; // set to true when debugging

    public CodeGen(SemanticAnalyzer analyzer){
        this.analyzer = analyzer;
        typeStack = new Stack();
        labelStack = new Stack();
        falseLabels = new Stack();
        endLabels = new Stack();
        loopLabels = new Stack();
        register = 0;
    }

    public void buildFile(){
        try{
            out = new BufferedWriter(new FileWriter(fileName));
        }
        catch(Exception e)
        {
            e.printStackTrace();
        }
    }

    public void close(){
        try {
            out.flush();
            out.close();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    public void comment(String comment)
    {
        if(!ErrorHandler.doErrorsExist())
        {
            write(";"+comment);
        }
        else{
            System.out.println("Error found before comment");
        }
    }

    public void setupProg(){
        if(!ErrorHandler.doErrorsExist()){
            write("PUSH D0");
            write("MOV SP D0");
            register = 0;
        }
        
    }

    //Function
    //Procedure REGISTER setup
    public void setup(String s) {
        if(!ErrorHandler.doErrorsExist()){
            //if a function, give one extra place of room for the return value
            comment("setup "+analyzer.lookupSymbol(s).getKind()+" "+analyzer.lookupSymbol(s).getLexeme());
            if(analyzer.lookupSymbol(s).getKind().equals("Function"))
            {
                write("ADD SP #1 SP");
                typeStack.push(analyzer.lookupSymbol(s).getType());
            }
            int level = analyzer.lookupSymbolDepth(s)+1;

            write("PUSH D"+level);
            //write("MOV SP D"+level);
            register = level;
        }
    }

    public void start(){
        if(!ErrorHandler.doErrorsExist()){
            //label procedure/functions for call
            //This is what we call to reach procedures/functions
            if(analyzer.getCurrentScope().getNestLevel() != 0){
                comment(analyzer.getCurrentScope().getName());
                write(analyzer.getCurrentScope().getLabel()+":");
            }
            //allocate space for local variables
            int size = analyzer.getSizeLocals(analyzer.getCurrentScope().getNestLevel());
            write("ADD SP #"+size+" SP");
        }
        else{
            System.out.println("Error found before startProg");
        }
    }

    public void end(){
        if(!ErrorHandler.doErrorsExist()){
            //remove allocated space for local variables from symbol table, not symbols
            int size = analyzer.getSizeLocals(analyzer.getCurrentScope().getNestLevel());
            write("SUB SP #"+size+" SP");
            //if level is > 0, then its a function/procedure
            //write RET after removing local variables
            if(analyzer.getCurrentScope().getNestLevel() != 0)
            {
                write("RET");
            }
        }
        else{
            System.out.println("Error found before endProg");
        }
    }

    //Function
    //Procedure teardown
    //requires teardown of passed parameters
    public void teardown(String name) {
        if(!ErrorHandler.doErrorsExist()){
            //Gather size of passed parameters for removal off the stack
            AbstractMethodSymbol sym = (AbstractMethodSymbol) analyzer.lookupSymbol(name);
            int parameters = sym.getSizeParameters();
            register = analyzer.lookupSymbolDepth(name)+1;
            write("SUB SP #"+parameters+" SP");//passed parameters
            write("POP D"+register);
        }
    }

    public void finish() {
        if(!ErrorHandler.doErrorsExist()){
            write("POP D0");
            write("HLT");
        }
    }

    public void writeParam(){
        //Used only after expression has been called
        //write the latest value on stack

        //"WRTS"
        if(!ErrorHandler.doErrorsExist()){
            write("WRTS");
        }
        else{
            System.out.println("Error found before writeParam");
        }
    }

    public void readParam(String lexeme){
        if(!ErrorHandler.doErrorsExist()){
            Symbol symbol = analyzer.lookupSymbol(lexeme);
            if(symbol.getKind().equals("Parameter"))
            {
                ParameterSymbol sym = (ParameterSymbol) analyzer.lookupSymbol(lexeme);
                if(sym.getMode() == SemanticRecord.MODE_REF)
                {
                    write("RD @"+symbol.getOffset()+"(D"+analyzer.lookupSymbolDepth(lexeme)+")");
                }
                else
                {
                    write("RD "+symbol.getOffset()+"(D"+analyzer.lookupSymbolDepth(lexeme)+")");
                }
            }
            else{
                write("RD "+symbol.getOffset()+"(D"+analyzer.lookupSymbolDepth(lexeme)+")");
            }
        }
        else{
            System.out.println("Error found before readParam");
        }
    }

    public void assign(String lexeme){
        if(!ErrorHandler.doErrorsExist()){
            //if the lexeme is the current function scope, mark as the return value
            if(analyzer.getCurrentScope().getName().equals(lexeme))
            {
                write("POP -2(D"+analyzer.getCurrentScope().getNestLevel()+")");
                typeStack.pop();
            }
            else{
                Symbol symbol = analyzer.lookupSymbol(lexeme);
                //else if the kind is parameter type, then do pointer arthimatic to return the right vlue
                if(analyzer.lookupSymbol(lexeme).getKind().equals("Parameter")){
                    ParameterSymbol sym = (ParameterSymbol) analyzer.lookupSymbol(lexeme);
                    if (sym.getMode() == SemanticRecord.MODE_REF)
                    {
                        write("POP @"+symbol.getOffset()+"(D"+analyzer.lookupSymbolDepth(lexeme)+")");
                    }
                    else
                    {
                        write("POP "+symbol.getOffset()+"(D"+analyzer.lookupSymbolDepth(lexeme)+")");
                    }
                }
                //else the popped value must be into a variable or other useable thing, not a function/procedure call etc.
                else if(analyzer.lookupSymbol(lexeme).getKind().equals("Variable")){
                    write("POP "+symbol.getOffset()+"(D"+analyzer.lookupSymbolDepth(lexeme)+")");
                }
                else{
                    ErrorHandler.disableCodeGeneration();
                    ErrorHandler.logError("Assign into wrong type - CodeGen");
                }
                typeStack.pop();
            }
        }
        else{
            System.out.println("Error found before assign");
        }
    }

    //used exclusively when inside a factor. Pushes the value onto the stack
    public void pushValue(Token token)
    {
        if(!ErrorHandler.doErrorsExist()){
            //Symbol symbol = analyzer.lookupSymbol(token.getLexeme());
            if (token.getType() == Token.MP_INTEGER_LIT)
            {
                Symbol symbol = new VariableSymbol(token, Symbol.TYPE_INT);
                            System.out.println(symbol.getTypeString());


                typeStack.push(symbol.getType());
                write("PUSH #"+token.getLexeme());
            }
            else if (token.getType() == Token.MP_FLOAT_LIT)
            {
                Symbol symbol = new VariableSymbol(token, Symbol.TYPE_FLOAT);
                            System.out.println(symbol.getTypeString());

                typeStack.push(symbol.getType());
                write("PUSH #"+token.getLexeme());
            }
        }
        else{
            System.out.println("Error found before pushValue");
        }
    }

    //pushes the local variable value onto stack
    public void pushVar(String id, String method, int plocation)
    {
        if(!ErrorHandler.doErrorsExist()){
            Symbol symbol = analyzer.lookupSymbol(id);
            System.out.println(symbol.getTypeString());
            //if method is not null, then this push is for a parameter
            if(method != null){
                Byte varMode;
                //find if the procedure/function location mode
                AbstractMethodSymbol sym = (AbstractMethodSymbol) analyzer.lookupSymbol(method);
                Byte passMode = sym.getParameters().get(plocation).getMode();
                //find the variable mode
                if(analyzer.lookupSymbol(id).getKind().equals("Parameter"))
                {
                    ParameterSymbol p = (ParameterSymbol)analyzer.lookupSymbol(id);
                    varMode = p.getMode();
                }
                else
                {
                    varMode = SemanticRecord.MODE_VAL;
                }

                //passmode,varmode
                //in,in
                if(passMode.equals(SemanticRecord.MODE_VAL) && varMode.equals(SemanticRecord.MODE_VAL)){
                    write("PUSH "+symbol.getOffset()+"(D"+analyzer.lookupSymbolDepth(id)+")");
                }
                //in/out,in
                else if(varMode.equals(SemanticRecord.MODE_REF) && passMode.equals(SemanticRecord.MODE_VAL)){
                    write("PUSH @"+symbol.getOffset()+"(D"+analyzer.lookupSymbolDepth(id)+")");
                }
                //in/out, in/out
                else if(passMode.equals(SemanticRecord.MODE_REF) && varMode.equals(SemanticRecord.MODE_REF)){
                    write("PUSH "+symbol.getOffset()+"(D"+analyzer.lookupSymbolDepth(id)+")");
                }
                //in, in/out
                else if(varMode.equals(SemanticRecord.MODE_VAL) && passMode.equals(SemanticRecord.MODE_REF)){
                    //pointer arithmatic!! <:O
                    write("PUSH #"+symbol.getOffset());
                    write("PUSH D"+analyzer.lookupSymbolDepth(id));
                    write("ADDS");
                }
                typeStack.push(symbol.getType());
            }
            else if(analyzer.lookupSymbol(id).getKind().equals("Parameter")){
                ParameterSymbol sym = (ParameterSymbol) analyzer.lookupSymbol(id);
                if (sym.getMode() == SemanticRecord.MODE_REF)
                {
                    typeStack.push(symbol.getType());
                    write("PUSH @"+symbol.getOffset()+"(D"+analyzer.lookupSymbolDepth(id)+")");
                }
                else
                {
                    typeStack.push(symbol.getType());
                    write("PUSH "+symbol.getOffset()+"(D"+analyzer.lookupSymbolDepth(id)+")");
                }
            }
            else{
                //else this is not for a parameter pass, just push value
                typeStack.push(symbol.getType());
                write("PUSH "+symbol.getOffset()+"(D"+analyzer.lookupSymbolDepth(id)+")");
            }
        }
        else{
            System.out.println("Error found before pushVar");
        }
    }

    public void neg(){
        if(!ErrorHandler.doErrorsExist()){
            if(typeStack.peek() == Symbol.TYPE_BOOLEAN)
            {
                String label1 = LabelGen.getLabelGen().getLabel();
                String label2 = LabelGen.getLabelGen().getLabel();
                write("BEQ -1(SP) #0 "+label1);
                write("SUB SP #1 SP");
                write("PUSH #0");
                write("BR "+label2);
                write(label1+":");
                write("SUB SP #1 SP");
                write("PUSH #1");
                write(label2+":");
            }
            else
            {
                ErrorHandler.logError("Cannot NOT a non-boolean");
            }
        }
        else{
            System.out.println("Error found before neg");
        }
    }
    
    public void addop(byte operator){
        if(!ErrorHandler.doErrorsExist()){
            Byte s1 = typeStack.pop();
            Byte s2 = typeStack.pop();
            switch (operator) {
                case Token.MP_PLUS:
                    if(s1 == s2){
                        write("ADDS");
                        typeStack.push(s1); //Type stays the same
                    }
                    else{
                        System.out.println("adding operators not the same type");
                    }
                    break;
                case Token.MP_MINUS:
                    if(s1 == s2){
                        write("SUBS");
                        typeStack.push(s1); //Type stays the same
                    }
                    else{
                        System.out.println(s1);
                        System.out.println(s2);
                        System.out.println("subtract operators not the same type");
                    }
                    break;
                case Token.MP_OR:
                    if(s1 == s2){
                        write("ADDS"); //adds, if 1 or 2, then true, 0 false
                        write("PUSH #0");
                        typeStack.push(s1);
                        typeStack.push(s2);
                        compare(Token.MP_NEQUAL);
                    }
                    else{
                        System.out.println("OR operators not the same type");
                    }
                    break;
                default:
                    System.out.println("Addop error");
            }
        }
    }

    public void multop(byte operator){
        if(!ErrorHandler.doErrorsExist()){
            Byte s1 = typeStack.pop();
            Byte s2 = typeStack.pop();
            switch (operator) {
                case Token.MP_TIMES:
                    if(s1 == s2){
                        write("MULS");
                        typeStack.push(s1); //Type stays the same
                    }
                    else{
                        System.out.println("multiplying operators not the same type");
                    }
                    break;
                case Token.MP_DIV:
                    if(s1 == s2){
                        write("DIVS");
                        typeStack.push(s1);
                    }
                    else{
                        System.out.println("Dividing operators not the same type");
                    }
                    break;
                case Token.MP_MOD:
                    if(s1 == s2){
                        /* !x mod y!
                         * while(x > y){
                         *      x = x-y;
                         * }
                         * return x
                         */
                        String label1 = LabelGen.getLabelGen().getLabel();
                        String label2 = LabelGen.getLabelGen().getLabel();
                        write(label1+":");
                        write("BLT -2(SP) -1(SP) "+label2);
                        write("SUB -2(SP) -1(SP) -2(SP)");
                        write("BR "+label1);
                        write(label2+":");
                        write("SUB SP #1 SP");
                        typeStack.push(s1);
                    }
                    else{
                        System.out.println("multiplying operators not the same type");
                    }
                    break;
                case Token.MP_AND:
                    if(s1 == s2){
                        write("MULS"); //0*0=0, 0*1=0, 1*1=1 || 1 is true
                        typeStack.push(s1);
                    }
                    else{
                        System.out.println("multiplying operators not the same type");
                    }
                    break;
                default:
                    System.out.println("Multop error");
            }
        }
    }

    private void write(String s){
        try{
            out.write(s);
            out.newLine();
            out.flush();
            if (screenlog){
                System.out.println(s);
            }
        }
        catch(Exception e){
            e.printStackTrace();
        }
    }

    public void optionalSign(byte sign) {
        if(!ErrorHandler.doErrorsExist()){
            try{
                switch(sign){
                    case Token.MP_PLUS:
                        //do nothing!
                        break;
                    case Token.MP_MINUS:
                        write("NEGS");
                        break;
                    default:
                        //-1, do nothing
                        break;
                }
            }
            catch(Exception e){
                e.printStackTrace();
            }
        }
    }

    public void compare(byte op) {
        if (!ErrorHandler.doErrorsExist()) {
            //2 values are on stack at this point
            //if false dont break every time, PUSH 0 to stack
            Byte s1 = typeStack.pop();
            Byte s2 = typeStack.pop();
            boolean error = true;
            if (!ErrorHandler.doErrorsExist()) {
                String label1 = LabelGen.getLabelGen().getLabel();
                String label2 = LabelGen.getLabelGen().getLabel();
                if (s1 == s2) {
                    error = false;
                    switch (op) {
                        case Token.MP_EQUAL:
                            write("BEQ -2(SP) -1(SP) " + label1);
                            break;
                        case Token.MP_LTHAN:
                            write("BLT -2(SP) -1(SP) " + label1);
                            break;
                        case Token.MP_GTHAN:
                            write("BGT -2(SP) -1(SP) " + label1);
                            break;
                        case Token.MP_LEQUAL:
                            write("BLE -2(SP) -1(SP) " + label1);
                            break;
                        case Token.MP_GEQUAL:
                            write("BGE -2(SP) -1(SP) " + label1);
                            break;
                        case Token.MP_NEQUAL:
                            write("BNE -2(SP) -1(SP) " + label1);
                            break;
                        default:
                            error = true;
                            System.out.println("Error in relational operator");
                    }
                }
                if (!error) {
                    write("SUB SP #2 SP");
                    write("PUSH #0"); //push 0 onto stack under false condition
                    write("BR " + label2); //move past
                    write(label1 + ":"); //if true
                    write("SUB SP #2 SP"); //remove values
                    write("PUSH #1"); //push 1 onto stack under true condition
                    write(label2 + ":");
                    typeStack.push(Symbol.TYPE_BOOLEAN); //set symbol type to boolean
                }
            }
        }
    }

    //-------------------------------------------------------
    // If else statement Semantic Actions
    //-------------------------------------------------------
    public void ifThenStatement() {
        if (!ErrorHandler.doErrorsExist()) {
            falseLabels.push(LabelGen.getLabelGen().getLabel());
            endLabels.push(LabelGen.getLabelGen().getLabel());
            write("BEQ -1(SP) #0 " + falseLabels.peek());
            comment("Then");
            write("SUB SP #1 SP");
        }
    }

    public void endIf() {
        if (!ErrorHandler.doErrorsExist()) {
            write(endLabels.pop() + ":");
        }

    }

    public void elseStatement() {
        if (!ErrorHandler.doErrorsExist()) {
            write(falseLabels.pop() + ":");
            write("SUB SP #1 SP");
        }
    }
   
    public void breakToEndIf() {
        if (!ErrorHandler.doErrorsExist()) {
            write("BR " + endLabels.peek());
        }
    }

    //-------------------------------------------------------
    // Looping statement Semantic Actions
    //-------------------------------------------------------

    public void repeatStatement() {
        if (!ErrorHandler.doErrorsExist()) {
            loopLabels.push(LabelGen.getLabelGen().getLabel());
            write("PUSH #0");
            write(loopLabels.peek() + ":");
            write("SUB SP #1 SP");
        }
    }

    public void endRepeat() {
        if (!ErrorHandler.doErrorsExist()) {
            write("BEQ -1(SP) #0 " + loopLabels.pop());
            write("SUB SP #1 SP");
        }
    }

    public void startWhile() {
        if (!ErrorHandler.doErrorsExist()) {
            loopLabels.push(LabelGen.getLabelGen().getLabel());
            write(loopLabels.peek() + ":");
        }
    }

    public void whileStatement() {
        if (!ErrorHandler.doErrorsExist()) {
            loopLabels.push(LabelGen.getLabelGen().getLabel());
            write("BEQ -1(SP) #0 " + loopLabels.peek());
            write("SUB SP #1 SP");
        }
    }

    public void endWhile() {
        if (!ErrorHandler.doErrorsExist()) {
            String l1 = loopLabels.pop();
            String l2 = loopLabels.pop();
            write("BR " + l2);
            write(l1 + ":");
            write("SUB SP #1 SP");
        }
    }

    public void startFor(String s) {
        if (!ErrorHandler.doErrorsExist()) {
            loopLabels.push(LabelGen.getLabelGen().getLabel());
            write(loopLabels.peek() + ":");
            pushVar(s, null, 0);
        }
    }

    public void forStatement() {
        if (!ErrorHandler.doErrorsExist()) {
            loopLabels.push(LabelGen.getLabelGen().getLabel());
            write("BEQ -1(SP) #0 " + loopLabels.peek());
            write("SUB SP #1 SP");
        }
    }

    public void endFor(String s, Byte op) {
        if (!ErrorHandler.doErrorsExist()) {
            String l1 = loopLabels.pop();
            String l2 = loopLabels.pop();

            pushVar(s, null, 0);
            //add increment to control variable
            switch(op){
                case Token.MP_LEQUAL: //(to) increment by 1
                    write("PUSH #1");
                    write("ADDS");
                    break;
                case Token.MP_GEQUAL: //(to) decrement by 1
                    write("PUSH #-1");
                    write("ADDS");
                    break;
                default:
                    System.out.println("Error in endFor");
            }
            assign(s);
            write("BR " + l2);
            write(l1 + ":");
            write("SUB SP #1 SP");
        }
    }

    //---------------------------------------------
    // nested procedure/function Semantic Actions
    //---------------------------------------------

    public void branchToBegin() {
        if (!ErrorHandler.doErrorsExist()) {
            String label1 = LabelGen.getLabelGen().getLabel();
            labelStack.push(label1);
            write("BR "+label1);
        }
    }

    public void beginLabel() {
        if (!ErrorHandler.doErrorsExist()) {
            String label = labelStack.pop();
            write(label+":");
        }
    }

    //Call a procedure or function
    public void call(String s) {
        if (!ErrorHandler.doErrorsExist()) {
            int level = analyzer.lookupSymbolDepth(s)+1;
            AbstractMethodSymbol sym = (AbstractMethodSymbol)analyzer.lookupSymbol(s);
            int offset = sym.getParameters().size();
            //move D1 to the new location, SP-# of parameters pushed onto the stack
            write("MOV SP D"+level);
            if(offset > 0){
                write("SUB D"+level+" #"+offset+" D"+level);
            }
            write("CALL "+analyzer.lookupSymbol(s).getLabel());
        }
    }

}
